package com.stray.crawl;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.client.params.CookiePolicy;
import org.apache.http.client.params.HttpClientParams;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.impl.client.DecompressingHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.impl.conn.SchemeRegistryFactory;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;

import com.stray.crawl.Constants.DefaultSetting;
import com.stray.crawl.excp.InitialException;
import com.stray.crawl.expr.ExpressExecutor;
import com.stray.crawl.model.dao.InMemoryPlanRepository;
import com.stray.crawl.model.dao.PlanRepository;
import com.stray.crawl.model.dao.SimpleEntityManager;
import com.stray.crawl.supt.FileStorage;
import com.stray.crawl.supt.LRUCache;
import com.stray.crawl.utils.IOUtils;
import com.stray.crawl.worker.HttpLogInterceptor;
import com.stray.crawl.worker.TaskService;
import com.stray.crawl.worker.task.CrawlTask;

public final class BeanContext {

	private static final Log LOG = LogFactory.getLog(BeanContext.class);

	// singleton instance
	private static BeanContext instance;
	private static Properties config;
	private static Map<Class<?>, Object> beansPool;

	// Data access
	private PlanRepository planRepository;
	private SimpleEntityManager entityManager;

	// Execution
	private ClientConnectionManager connectionManager;
	private HttpClient httpClient;
	private ExecutorService executorService;
	private ExpressExecutor expressExecutor;
	private LRUCache<CrawlTask, Future<ResultContext>> resultsCache;
	private TaskService taskService;

	// Support
	private FileStorage fileStorage;

	static {
		try {
			config = IOUtils.readProperties(BeanContext.class, "config.properties");
			beansPool = new HashMap<Class<?>, Object>();
		} catch (InitialException ex) {
			LOG.error("Fail to initialize beans instance. ", ex);
		} catch (IOException ex) {
			LOG.error("Fail to read file beans.properties. ", ex);
		}
	}
	
	public static BeanContext getContext() {
		if (instance == null) {
			synchronized (BeanContext.class) {
				instance = new BeanContext();
			}
		}

		return instance;
	}

	@SuppressWarnings("unchecked")
	public static <T> T get(Class<T> clazz) {
		return (T) beansPool.get(clazz);
	}

	public static String getConfig(String key, String defValue) {
		String value = config.getProperty(key);
		return value != null ? value : defValue;
	}

	public static int getIntConfig(String key, int defValue) {
		String value = config.getProperty(key);

		if (value == null || value.length() == 0) {
			return defValue;
		}

	    for (char c : value.toCharArray()) {
			if (!Character.isDigit(c)) {
				return defValue;
			}
		}

		return Integer.valueOf(value);
	}

	private BeanContext() {
		try {
			// Extract Parameter
			int liveTime = getIntConfig("connection.live_time", DefaultSetting.CONNECTION_LIVE_TIME);
			int maxPerRoute = getIntConfig("connection.max_per_route", DefaultSetting.MAX_PER_ROUTE);
			int maxConcurrent = getIntConfig("connection.max_concurrent", DefaultSetting.MAX_CONCURRENT);

			int timeout = getIntConfig("connection.timeout", DefaultSetting.CONNECTION_TIMEOUT);
			String useragent = getConfig("crawl.user_agent", DefaultSetting.USER_AGENT);
			String charset = getConfig("crawl.charset", DefaultSetting.CHARSET);

			int histCount = getIntConfig("storage.cache.history_record_count", DefaultSetting.HISTORY_RECORD_COUNT);
			String sqliteDbUrl = getConfig("storage.sqlite.dburl", DefaultSetting.DB_URL);

			String rootPath = getConfig("storage.file.root_path", DefaultSetting.ROOT_PATH);

			// Create result cache
			resultsCache = new LRUCache<CrawlTask, Future<ResultContext>>(histCount);
			beansPool.put(LRUCache.class, resultsCache);
			
			// Create storage
			fileStorage = new FileStorage(rootPath);
			beansPool.put(FileStorage.class, fileStorage);
			
			// Create em
			entityManager = new SimpleEntityManager(sqliteDbUrl);
			beansPool.put(SimpleEntityManager.class, entityManager);

			// Create connection manager
			SchemeRegistry registry = SchemeRegistryFactory.createDefault();
			PoolingClientConnectionManager connMgr = new PoolingClientConnectionManager(registry, liveTime, TimeUnit.MINUTES);
			connMgr.setMaxTotal(maxConcurrent);
			connMgr.setDefaultMaxPerRoute(maxPerRoute);
			connectionManager = connMgr;
			beansPool.put(ClientConnectionManager.class,  connectionManager);

			// Create http client
			HttpParams params = new BasicHttpParams();
			HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
			HttpProtocolParams.setUseExpectContinue(params, false);
			HttpProtocolParams.setUserAgent(params, useragent);
			HttpProtocolParams.setContentCharset(params, charset);
			HttpClientParams.setRedirecting(params, true);
			HttpClientParams.setCookiePolicy(params, CookiePolicy.BROWSER_COMPATIBILITY);
			HttpConnectionParams.setConnectionTimeout(params, timeout);

			DefaultHttpClient client = new DefaultHttpClient(connectionManager, params);
			HttpLogInterceptor itcp = new HttpLogInterceptor();
			client.addRequestInterceptor(itcp);
			client.addResponseInterceptor(itcp);

			httpClient = new DecompressingHttpClient(client);
			beansPool.put(HttpClient.class,  httpClient);

			// Create executor service
			executorService = Executors.newFixedThreadPool(maxConcurrent, new ThreadFactory() {

				private int count = 1;

				@Override
				public Thread newThread(Runnable r) {
					return new Thread(r, "task-service-thread-" + count++);
				}

			});
			beansPool.put(ExecutorService.class,  executorService);

			// Create express executor
			expressExecutor = new ExpressExecutor();
			beansPool.put(ExpressExecutor.class, expressExecutor);
			
			// Create plan repository
//			planRepository = new SQLitePlanRepository();
			planRepository = new InMemoryPlanRepository();
			beansPool.put(PlanRepository.class, planRepository);
			
			// Create service
			taskService = new TaskService();
			beansPool.put(TaskService.class,  taskService);
		} catch (Exception ex) {
			try {
				close();
			} catch (Exception e) {
				// ignore
			}

			throw new InitialException(ex);
		}
	}

	public void close() {
		synchronized (BeanContext.class) {
			// Clear singleton
			instance = null;

			// close resources
			try {
				entityManager.close();
			} catch (Exception e) {
				// ignore
			}

			if (connectionManager != null) {
				try {
					connectionManager.shutdown();
				} catch (Exception e) {
					// ignore
				}
			}

			if (executorService != null) {
				try {
					executorService.shutdownNow();
				} catch (Exception e) {
					// ignore
				}
			}
		}
	}

}